//---------------------------------------------------------------------
// Author: Keith Hill
//
// Description: Class to implement the Get-CmdletMaml cmdlet.
//
// Creation Date: Dec 9, 2006
//---------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Security;
using System.Text;
using System.Xml;

namespace Pscx.Commands
{
    [Obsolete("Use Get-PSSnapinHelp from PscxDeveloper and Update-PscxHelp.ps1 from PsxcHelp")]
    [Cmdlet(VerbsCommon.Get, "CmdletMaml", DefaultParameterSetName = "Path"),
     Description("Reflects on the .NET assembly at the specified path, finds all types with the CmdletAttribute and generates a skeleton MAML help for the assembly.")]
    public class GetCmdletMamlCommand : PSCmdlet
    {
        private const string mamlNS = "http://schemas.microsoft.com/maml/2004/10";
        private const string cmdNS = "http://schemas.microsoft.com/maml/dev/command/2004/10";
        private const string devNS = "http://schemas.microsoft.com/maml/dev/2004/10";
        private string[] _paths;
        private bool _literalPathUsed;
        private bool _all;
        private string _encodingParam = "utf8";
        private Encoding _encoding;
        MemoryStream _outputStream;
        XmlWriter _xmlWriter;
        List<CmdletInfo> _cmdletInfo = new List<CmdletInfo>();

        [Parameter(ParameterSetName = "Path",
                   HelpMessage = "Specifies the path to the PowerShell snapin assemblies to process. Wildcard syntax is allowed.",
                   Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true),
         AcceptsWildcards(true)]
        public string[] Path
        {
            get { return _paths; }
            set { _paths = value; }
        }

        [Alias("PSPath"),
         Parameter(ParameterSetName = "LiteralPath",
                   HelpMessage = "Specifies a path to the item. The value of -LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell Windows PowerShell not to interpret any characters as escape sequences.",
                   Position = 0, Mandatory = true, ValueFromPipeline = false, ValueFromPipelineByPropertyName = true),
         AcceptsWildcards(false)]
        public string[] LiteralPath
        {
            get { return _paths; }
            set
            {
                _paths = value;
                _literalPathUsed = true;
            }
        }

        [Parameter(HelpMessage = "Generates MAML for all sections, even the optional ones like Notes, Examples, etc.")]
        public SwitchParameter All
        {
            get { return (SwitchParameter)_all; }
            set { _all = (bool)value; }
        }

        [Parameter(HelpMessage = "The encoding to use for the XML."),
         AcceptsWildcards(false),
         DefaultValue("UTF8")]
        public string Encoding
        {
            get { return _encodingParam; }
            set { _encodingParam = value; }
        }

        protected override void BeginProcessing()
        {
            try
            {
                _encoding = Utils.GetEncodingFromName(_encodingParam);
            }
            catch (ArgumentException ex)
            {
                ThrowTerminatingError(new ErrorRecord(ex, "InvalidArgumentError", ErrorCategory.InvalidArgument, _encodingParam));
            }

            _outputStream = new MemoryStream();
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.ConformanceLevel = ConformanceLevel.Document;
            settings.Encoding = _encoding;
            settings.Indent = true;
            _xmlWriter = XmlWriter.Create(_outputStream, settings);
            _xmlWriter.WriteStartDocument();
            _xmlWriter.WriteStartElement("helpItems", "http://msh");
            _xmlWriter.WriteAttributeString("schema", "maml");
        }

        protected override void ProcessRecord()
        {
            if (_paths != null)
            {
                List<string> pathList = Utils.ResolveFilePaths(this, _paths, _literalPathUsed);
                foreach (string path in pathList)
                {
                    WriteDebug("Processing file " + path);
                    ProcessAssembly(path);
                }
            }
        }

        protected override void EndProcessing()
        {
            _xmlWriter.WriteEndElement();  // </helpItems>
            _xmlWriter.WriteEndDocument();
            _xmlWriter.Flush();

            byte[] bytes = Utils.RemoveByteOrderMark(_outputStream);
            _outputStream.Dispose();
            _outputStream = null;
            _xmlWriter = null;

            string result = _encoding.GetString(bytes);
            WriteObject(result);
        }

        private void ProcessAssembly(string path)
        {
            Assembly assembly = null;
            try
            {
                assembly = Assembly.LoadFrom(path);
            }
            catch (Exception ex)
            {
                WriteError(new ErrorRecord(ex, "FailedAssemblyLoadFrom", ErrorCategory.InvalidArgument, path));
            }

            if (assembly == null) return;


            foreach (Type type in assembly.GetExportedTypes())
            {
                ProcessType(type);
            }

            foreach (CmdletInfo cmdletInfo in _cmdletInfo)
            {
                WriteCmdletMaml(cmdletInfo);
            }
        }

        private void ProcessType(Type type)
        {
            object[] attrs = type.GetCustomAttributes(false);
            foreach (object attr in attrs)
            {
                if (attr is CmdletAttribute)
                {
                    CmdletAttribute cmdletAttr = (CmdletAttribute)attr;
                    ProcessCmdlet(type, cmdletAttr);
                }
            }
        }

        private void ProcessCmdlet(Type type, CmdletAttribute cmdletAttr)
        {
            CmdletInfo cmdletInfo = new CmdletInfo();
            _cmdletInfo.Add(cmdletInfo);
            cmdletInfo.Verb = cmdletAttr.VerbName;
            cmdletInfo.Noun = cmdletAttr.NounName;

            object[] attrs = type.GetCustomAttributes(false);
            foreach (object attr in attrs)
            {
                if (attr is DescriptionAttribute)
                {
                    DescriptionAttribute descAttr = (DescriptionAttribute)attr;
                    cmdletInfo.Description = descAttr.Description;
                }
                else if (attr is DetailedDescriptionAttribute)
                {
                    DetailedDescriptionAttribute detDescAttr = (DetailedDescriptionAttribute)attr;
                    cmdletInfo.DetailedDescription = detDescAttr.Text;
                }
                else if (attr is RelatedLinksAttribute)
                {
                    RelatedLinksAttribute linkAttr = (RelatedLinksAttribute)attr;
                    cmdletInfo.RelatedLinks.Add(linkAttr.Text);
                }
            }

            PropertyInfo[] props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            foreach (PropertyInfo prop in props)
            {
                attrs = prop.GetCustomAttributes(false);
                foreach (object attr in attrs)
                {
                    if (attr is ParameterAttribute)
                    {
                        ProcessParameter(cmdletInfo, (ParameterAttribute)attr, prop);
                    }
                }
            }
        }

        private void ProcessParameter(CmdletInfo cmdletInfo, ParameterAttribute parameterAttr, PropertyInfo propertyInfo)
        {
            ParameterInfo parameterInfo = new ParameterInfo();
            parameterInfo.Name = propertyInfo.Name;
            parameterInfo.Type = propertyInfo.PropertyType.Name;
            parameterInfo.HelpMessage = parameterAttr.HelpMessage;
            parameterInfo.Required = parameterAttr.Mandatory;
            if (parameterAttr.Position >= 0)
            {
                parameterInfo.Position = parameterAttr.Position;
            }
            parameterInfo.ValueFromPipeline = parameterAttr.ValueFromPipeline;
            parameterInfo.ValueFromPipelineByPropertyName = parameterAttr.ValueFromPipelineByPropertyName;
            if (String.Compare(parameterAttr.ParameterSetName, "__AllParameterSets", true) == 0)
            {
                cmdletInfo.AllParametersParameterSet.Add(parameterInfo);
            }
            else
            {
                if (!cmdletInfo.NamedParameterSets.ContainsKey(parameterAttr.ParameterSetName))
                {
                    List<ParameterInfo> parameterSet = new List<ParameterInfo>();
                    cmdletInfo.NamedParameterSets.Add(parameterAttr.ParameterSetName, parameterSet);
                }
                cmdletInfo.NamedParameterSets[parameterAttr.ParameterSetName].Add(parameterInfo);
            }

            object[] attrs = propertyInfo.GetCustomAttributes(false);
            foreach (object attr in attrs)
            {
                if (attr is DefaultValueAttribute)
                {
                    DefaultValueAttribute defaultValueAttr = (DefaultValueAttribute)attr;
                    parameterInfo.DefaultValue = defaultValueAttr.Value.ToString();
                }
                else if (attr is AcceptsWildcardsAttribute)
                {
                    AcceptsWildcardsAttribute wildCardAttr = (AcceptsWildcardsAttribute)attr;
                    parameterInfo.AcceptsWildcards = wildCardAttr.Value;
                }
            }

            if ((parameterInfo.DefaultValue == String.Empty) && (parameterInfo.Type == "SwitchParameter"))
            {
                parameterInfo.DefaultValue = "false";
            }
        }

        private void WriteCmdletMaml(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment(cmdletInfo.Name + " HELP");
            _xmlWriter.WriteStartElement("command", "command", cmdNS);
            _xmlWriter.WriteAttributeString("xmlns", "maml", null, mamlNS);
            _xmlWriter.WriteAttributeString("xmlns", "dev", null, devNS);

            WriteCmdletMetaData(cmdletInfo);
            WriteDetailedDescription(cmdletInfo);
            WriteSyntax(cmdletInfo);
            WriteParameters(cmdletInfo);
            if (_all)
            {
                WriteErrorsThrown();
                WriteNotes();
                WriteExamples();
            }
            if (_all || (cmdletInfo.RelatedLinks.Count > 0))
            {
                WriteRelatedLinks(cmdletInfo);
            }
            _xmlWriter.WriteEndElement();
        }

        private void WriteCmdletMetaData(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment("METADATA");
            _xmlWriter.WriteStartElement("details", cmdNS);
            _xmlWriter.WriteStartElement("name", cmdNS);
            _xmlWriter.WriteString(cmdletInfo.Name);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("description", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString(cmdletInfo.Description);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("copyright", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString("");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("verb");
            _xmlWriter.WriteString(cmdletInfo.Verb);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("noun");
            _xmlWriter.WriteString(cmdletInfo.Noun);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("version", devNS);
            _xmlWriter.WriteString("");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
        }

        private void WriteDetailedDescription(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment("DETAILED DESCRIPTION");
            _xmlWriter.WriteStartElement("description", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            if (!String.IsNullOrEmpty(cmdletInfo.DetailedDescription))
            {
                _xmlWriter.WriteString(cmdletInfo.DetailedDescription);
            }
            else
            {
                _xmlWriter.WriteString(cmdletInfo.Description);
            }
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
        }

        private void WriteSyntax(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment("SYNTAX");
            _xmlWriter.WriteStartElement("syntax", cmdNS);

            if (cmdletInfo.NamedParameterSets.Count > 0)
            {
                foreach (KeyValuePair<string, List<ParameterInfo>> parameterSetEntry in cmdletInfo.NamedParameterSets)
                {
                    _xmlWriter.WriteComment("PARAMETERSET: " + parameterSetEntry.Key);
                    List<ParameterInfo> combinedParams = new List<ParameterInfo>(parameterSetEntry.Value);
                    combinedParams.AddRange(cmdletInfo.AllParametersParameterSet);
                    WriteSyntaxItem(cmdletInfo, combinedParams);
                }
            }
            else
            {
                _xmlWriter.WriteComment("ALL PARAMETERS");
                WriteSyntaxItem(cmdletInfo, cmdletInfo.AllParametersParameterSet);
            }

            _xmlWriter.WriteEndElement();
        }

        private void WriteSyntaxItem(CmdletInfo cmdletInfo, List<ParameterInfo> parameterSet)
        {
            _xmlWriter.WriteStartElement("syntaxItem", cmdNS);
            _xmlWriter.WriteStartElement("name", mamlNS);
            _xmlWriter.WriteString(cmdletInfo.Name);
            _xmlWriter.WriteEndElement();
            foreach (ParameterInfo parameterInfo in parameterSet)
            {
                WriteParameter(parameterInfo, false);
            }
            _xmlWriter.WriteEndElement();
        }

        private void WriteParameters(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment("PARAMETERS");
            _xmlWriter.WriteStartElement("parameters", cmdNS);

            foreach (KeyValuePair<string, List<ParameterInfo>> parameterSetEntry in cmdletInfo.NamedParameterSets)
            {
                foreach (ParameterInfo parameterInfo in parameterSetEntry.Value)
                {
                    WriteParameter(parameterInfo, true);
                }
            }
            foreach (ParameterInfo parameterInfo in cmdletInfo.AllParametersParameterSet)
            {
                WriteParameter(parameterInfo, true);
            }

            _xmlWriter.WriteEndElement();
        }

        private void WriteParameter(ParameterInfo parameterInfo, bool includeDevElements)
        {
            _xmlWriter.WriteStartElement("parameter", cmdNS);
            _xmlWriter.WriteStartAttribute("required");
            _xmlWriter.WriteValue(parameterInfo.Required);
            _xmlWriter.WriteEndAttribute();
            _xmlWriter.WriteStartAttribute("position");
            if (parameterInfo.Position >= 0)
            {
                _xmlWriter.WriteValue(parameterInfo.Position+1);
            }
            else
            {
                _xmlWriter.WriteString("named");
            }
            _xmlWriter.WriteEndAttribute();

            if (parameterInfo.AcceptsWildcards.HasValue || !parameterInfo.Type.ToLower().StartsWith("string"))
            {
                _xmlWriter.WriteStartAttribute("globbing");
                string value = parameterInfo.AcceptsWildcards.HasValue ? parameterInfo.AcceptsWildcards.ToString().ToLower() : "false";
                _xmlWriter.WriteValue(value);
                _xmlWriter.WriteEndAttribute();
            }

            string pipelineInput = "false";
            if (parameterInfo.ValueFromPipeline && parameterInfo.ValueFromPipelineByPropertyName)
            {
                pipelineInput = "true (ByValue, ByPropertyName)";
            }
            else if (parameterInfo.ValueFromPipeline)
            {
                pipelineInput = "true (ByValue)";
            }
            else if (parameterInfo.ValueFromPipelineByPropertyName)
            {
                pipelineInput = "true (ByPropertyName)";
            }
            _xmlWriter.WriteStartAttribute("pipelineInput");
            _xmlWriter.WriteString(pipelineInput);
            _xmlWriter.WriteEndAttribute();

            _xmlWriter.WriteStartElement("name", mamlNS);
            _xmlWriter.WriteString(parameterInfo.Name);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("description", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString(parameterInfo.HelpMessage);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("parameterValue", cmdNS);
            _xmlWriter.WriteString(parameterInfo.Type);
            _xmlWriter.WriteEndElement();

            if (includeDevElements)
            {
                _xmlWriter.WriteStartElement("type", devNS);
                _xmlWriter.WriteStartElement("name", mamlNS);
                _xmlWriter.WriteString(parameterInfo.Type);
                _xmlWriter.WriteEndElement();
                _xmlWriter.WriteStartElement("uri", mamlNS);
                _xmlWriter.WriteEndElement();
                _xmlWriter.WriteEndElement();

                _xmlWriter.WriteStartElement("defaultValue", devNS);
                _xmlWriter.WriteString(parameterInfo.DefaultValue);
                _xmlWriter.WriteEndElement();
            }

            _xmlWriter.WriteEndElement();
        }

        private void WriteErrorsThrown()
        {
            _xmlWriter.WriteComment("OPTIONAL: ERRORS THROWN");
            _xmlWriter.WriteStartElement("terminatingErrors", cmdNS);
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("nonTerminatingErrors", cmdNS);
            _xmlWriter.WriteEndElement();
        }

        private void WriteNotes()
        {
            _xmlWriter.WriteComment("OPTIONAL: NOTES");
            _xmlWriter.WriteStartElement("alertSet", mamlNS);
            _xmlWriter.WriteStartElement("title", mamlNS);
            _xmlWriter.WriteWhitespace("");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("alert", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString("Add content for Remark 1.");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
        }

        private void WriteExamples()
        {
            _xmlWriter.WriteComment("OPTIONAL: EXAMPLES");
            _xmlWriter.WriteStartElement("examples", cmdNS);
            _xmlWriter.WriteStartElement("example", cmdNS);
            _xmlWriter.WriteStartElement("title", mamlNS);
            _xmlWriter.WriteString("------------------------ EXAMPLE 1 ------------------------");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("introduction", mamlNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString("Add content for Remark 1.");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("code", devNS);
            _xmlWriter.WriteString("Example code.");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteStartElement("remarks", devNS);
            _xmlWriter.WriteStartElement("para", mamlNS);
            _xmlWriter.WriteString("Remarks about the example.");
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
            _xmlWriter.WriteEndElement();
        }

        private void WriteRelatedLinks(CmdletInfo cmdletInfo)
        {
            _xmlWriter.WriteComment("OPTIONAL: RELATED LINKS");
            _xmlWriter.WriteStartElement("relatedLinks", mamlNS);

            foreach (string link in cmdletInfo.RelatedLinks)
            {
                _xmlWriter.WriteStartElement("navigationLink", mamlNS);
                _xmlWriter.WriteStartElement("linkText", mamlNS);
                _xmlWriter.WriteString(link);
                _xmlWriter.WriteEndElement();
                _xmlWriter.WriteStartElement("uri", mamlNS);
                _xmlWriter.WriteEndElement();
                _xmlWriter.WriteEndElement();
            }
            _xmlWriter.WriteEndElement();
        }
    }

    internal sealed class CmdletInfo
    {
        public string Verb = String.Empty;
        public string Noun = String.Empty;
        public string Description = String.Empty;
        public string DetailedDescription = String.Empty;
        public Dictionary<string, List<ParameterInfo>> NamedParameterSets;
        public List<ParameterInfo> AllParametersParameterSet;
        public List<string> RelatedLinks;

        public CmdletInfo()
        {
            NamedParameterSets = new Dictionary<string, List<ParameterInfo>>(StringComparer.OrdinalIgnoreCase);
            AllParametersParameterSet = new List<ParameterInfo>();
            RelatedLinks = new List<string>();
        }

        public string Name
        {
            get { return Verb + "-" + Noun; }
        }
    }

    internal sealed class ParameterInfo
    {
        public string Name = String.Empty;
        public string Type = String.Empty;
        public string HelpMessage = String.Empty;
        public string DefaultValue = String.Empty;
        public bool Required;
        public int Position;
        public bool? AcceptsWildcards;
        public bool ValueFromPipeline;
        public bool ValueFromPipelineByPropertyName;

        public ParameterInfo()
        {
            Position = -1;
        }
    }
}

